// ActionScript file
import client.CourseGraphLayout;
import client.Student;
import client.TermListItemValueObject;

import flare.data.DataSet;
import flare.data.DataSource;
import flare.query.methods._;
import flare.query.methods.range;
import flare.vis.data.Data;
import flare.vis.data.NodeSprite;
import flare.vis.events.SelectionEvent;

import flash.display.Sprite;
import flash.events.Event;
import flash.utils.Dictionary;

import mx.collections.ArrayCollection;
import mx.collections.Sort;
import mx.containers.Panel;
import mx.controls.CheckBox;
import mx.controls.Menu;
import mx.controls.VSlider;
import mx.events.MenuEvent;
import mx.events.SliderEvent;

private var loader:URLLoader;

private var rawData:DataSet;

[Bindable]
private var termsArr:ArrayCollection;

[Bindable]
private var courseNodesArray:ArrayCollection;
private var courseNodes:Dictionary;
private var students:Dictionary;
private var termSequence:ArrayCollection;
[Bindable]
private var maxEdgeWeight:uint = 1;
[Bindable]
private var minEdgeWeight:uint = 0;


public function main():void {
	// load the data
	var ds:DataSource = new DataSource("CSMScourses.txt", "tab");
	loader = ds.load();
	loader.addEventListener(Event.COMPLETE, initData);  // function to handle data once loading is complete
}

public function nodeSpriteForRecord(record:Object, aggregateYears:Boolean):NodeSprite {
	var n:NodeSprite = new NodeSprite();
	n.data.quarter = record["Quarter"];
	n.data.subject = record["Class Subject"];
	n.data.number = record["Class Catalog Nbr"];
	n.data.students = new ArrayCollection();
	n.data.childNodes = new Dictionary();
	n.data.childNodeWeights = new Dictionary();
	n.data.parentNodes = new Dictionary();
	n.data.parentNodeWeights = new Dictionary();
	
	if (aggregateYears) {
		n.data.year = "";
		n.data.term = n.data.quarter;
	} else {
		n.data.year = record["Year"];
		n.data.term = n.data.year + " " + n.data.quarter
	}
	
	n.data.id = n.data.term + " " + n.data.subject + " " + n.data.number;
	
	return n;
}

public function graphForCourseNodes(nodes:Dictionary, directed:Boolean):Data {
	var graph:Data = new Data();
	var courseNode:NodeSprite;
	for each (courseNode in nodes) {
		graph.addNode(courseNode);
	}
	for each (courseNode in nodes) {
		for each (var childCourseNode:NodeSprite in courseNode.data.childNodes) {
			graph.addEdgeFor(courseNode, childCourseNode, directed, {weight: courseNode.data.childNodeWeights[childCourseNode.data.id] });
		}
	}
	
	return graph;
}

public function initData(evt:Event):void {
	rawData = loader.data as DataSet;
	resetData(false);
	holder.addChild(new CourseGraphLayout(null, null));
	edgeWeightSlider.labels = [minEdgeWeight, maxEdgeWeight];
}

public function resetData(aggregateYears:Boolean):void {
	var student:Student;
	var courseNode:NodeSprite;
	var terms:Dictionary = new Dictionary();
	
	// reinitialize globals
	students = new Dictionary();
	courseNodes = new Dictionary();
	courseNodesArray = new ArrayCollection();
	termsArr = new ArrayCollection();
	
	// parse the data
	for each (var record:Object in rawData.nodes.data) {
		// create or find the course
		courseNode = nodeSpriteForRecord(record, aggregateYears);
		if (!courseNodes[courseNode.data.id]) {
			courseNodes[courseNode.data.id] = courseNode;
			courseNodesArray.addItem(courseNode);
		} else {
			courseNode = courseNodes[courseNode.data.id];
		}
		
		// add the term to terms
		terms[courseNode.data.term] = { term: courseNode.data.term,
			year: courseNode.data.year,
			quarter: courseNode.data.quarter };
		
		// create or find the student
		student = new Student(record["ID Number"], record["Acad Career"]);
		if (!students[student.id]) {
			students[student.id] = student;
		} else {
			student = students[student.id];
		}
		
		// add student to courseNode
		courseNode.data.students.addItem(student);
		
		// add courseNode to student
		student.courses[courseNode.data.id] = courseNode;
	}
	
	// set up the termSequence
	var termObjectSequence:ArrayCollection = new ArrayCollection();
	for each (var t:Object in terms) {
		termObjectSequence.addItem(t);
	}
	// sort the termSequence chronologically
	var sorter:Sort = new Sort();
	var quarterMap:Object = {"Aut":0, "Wtr":1, "Spr":2, "Sum":3};
	sorter.compareFunction = function(a:Object, b:Object, fields:Array = null):int {
		if ( a.year < b.year ) {
			// a comes before b
			return -1;
		} else if (b.year < a.year) {
			return 1;
		} else {
			if (quarterMap[a.quarter] < quarterMap[b.quarter]) {
				return -1;
			} else if (quarterMap[b.quarter] < quarterMap[a.quarter]) {
				return 1;
			} else {
				return 0;
			}
		}
	}
	termObjectSequence.sort = sorter;
	termObjectSequence.refresh();
	
	// add the items to display in the mx:List that shows the terms
	for each (var o:Object in termObjectSequence) {
		var item:TermListItemValueObject = new TermListItemValueObject();
		item.label = o.term;
		item.selected = true;
		termsArr.addItem(item);
	}
	
	// update the courses filter list & UI
	var replacementCourses:Dictionary = new Dictionary();
	var oldKey:String;
	var newKey:String;
	for each (courseNode in courseAutoComplete.selectedItems) {
		// get list of old courses
		oldKey = courseNode.data.quarter + courseNode.data.subject + courseNode.data.number;
		replacementCourses[oldKey] = false;
	}
	
	// clear the old courses
	courseAutoComplete.clear();
	
	// find the new courses, setting them as keys of oldCourses
	for (oldKey in replacementCourses) {
		for each (courseNode in courseNodes) {
			newKey = courseNode.data.quarter + courseNode.data.subject + courseNode.data.number;
			if (oldKey == newKey) {
				if (aggregateYears) {
					// get same course (separate -> aggregate)
					replacementCourses[oldKey] = courseNode;
				} else {
					// get most recent (aggregate -> separate)
					if (!replacementCourses[oldKey] || replacementCourses[oldKey].data.year < courseNode.data.year) {
						replacementCourses[oldKey] = courseNode;
					}
				}
			}
		}
	}
	
	// add the new courses
	for each (courseNode in replacementCourses) {
		courseAutoComplete.selectedItems.addItem(courseNode);
	}
	
	courseAutoComplete.dataProvider = courseNodesArray;
}

public function connectCourses(redrawStudents:Dictionary):void {
	var student:Student;
	var courseNode:NodeSprite;
	// set up the edges
	for each (student in redrawStudents) {
		for each (courseNode in student.courses) {
			for each(var otherCourseNode:NodeSprite in student.courses) {
				setSuccessionRelationships(courseNode, otherCourseNode);
			}
		}
	}
}

public function redrawGraph():void {
	var redrawCourseNodes:Dictionary = new Dictionary();
	var redrawStudents:Dictionary = new Dictionary();
	var courseNode:NodeSprite;
	var student:Student;
	
	// clear all the course relationships
	for each (courseNode in courseNodes) {
		courseNode.data.childNodes = new Dictionary();
		courseNode.data.childNodeWeights = new Dictionary();
		courseNode.data.parentNodes = new Dictionary();
		courseNode.data.parentNodeWeights = new Dictionary();
	}
	
	// build the list of redrawCourseNodes to draw
	// and the list of redrawCourseNodes for each student in redrawStudents
	for each (student in students) {
		var redrawStudent:Student = new Student(student.id, student.career);
		var addedCourse:Boolean = false;
		
		for each (courseNode in student.courses) {
			if (!termsArrContains(courseNode.data.term) || !coursesFilterContains(courseNode.data.id)) {
				// if terms Array doesn't contain the quarter for this class
				// or the coursesFilterArray doesn't contain the class
				continue;
			}
			
			addedCourse = true;
			redrawCourseNodes[courseNode.data.id] = courseNode;
			redrawStudent.courses[courseNode.data.id] = courseNode;
		}
		if (addedCourse) {
			redrawStudents[redrawStudent.id] = redrawStudent;
		}
	}
	
	//Build term sequence selected by user
	termSequence = new ArrayCollection();
	var termObj:TermListItemValueObject;
	for each (termObj in termsArr) {
		if (termObj.selected) {
			termSequence.addItem(termObj.label);
		}
	}
	
	connectCourses(redrawStudents);
	
	var minWeight:uint = uint.MAX_VALUE;
	var maxWeight:uint = uint.MIN_VALUE;
	for each (courseNode in redrawCourseNodes) {
		for each (var weight:uint in courseNode.data.childNodeWeights) {
			if (weight < minWeight) {			
				minWeight = weight;
			}
			if (weight > maxWeight) {
				maxWeight = weight;
			}
		}
	}
	maxEdgeWeight = maxWeight;
	minEdgeWeight = minWeight;
	if (minWeight == uint.MAX_VALUE) {
		minWeight = 0;
	}
	if (maxWeight == uint.MIN_VALUE) {
		maxWeight = 1;	
	}
	edgeWeightSlider.labels = [minWeight, maxWeight];
	edgeWeightSlider.values = [minWeight, maxWeight];
	
	var courseGraph:Data = graphForCourseNodes(redrawCourseNodes, termFilterPanel.enabled);
	// set up the layout
	var courseGraphLayout:CourseGraphLayout = new CourseGraphLayout(courseGraph, expandNodeHandler);
	
	if (forceDirectedRadio.selected) {
		courseGraphLayout.setForceDirectedLayout();
	} else if (chronologicalRadio.selected) {
		courseGraphLayout.setChronologicalLayout();
	} else {
		courseGraphLayout.setForceDirectedLayout(true);	// true to disable forces for a manual layout
	}
	
	courseGraphLayout.play();
	
	holder.removeChildAt(0);
	holder.addChildAt(courseGraphLayout, 0);
	
	CourseGraphLayout(holder.getChildAt(0)).minEdgeWeightThreshold = minWeight;
	CourseGraphLayout(holder.getChildAt(0)).maxEdgeWeightThreshold = maxWeight;
	totalCoursesFilterLabel.text = courseAutoComplete.selectedItems.length + " courses selected";
}

public function setSuccessionRelationships(courseNodeA:NodeSprite, courseNodeB:NodeSprite):Object {
	var indexA:int = -10;
	var indexB:int = -10;
	var aFollowsB:Boolean = false;
	var bFollowsA:Boolean = false;
	var response:Object = { aFollowsB: false, bFollowsA: false };
	// determine term indices for courseNodes A and B
	for (var i:int=0; i<termSequence.length; i++) {
		if (termSequence[i] == courseNodeA.data.term) {
			indexA = i;
		}
		if (termSequence[i] == courseNodeB.data.term) {
			indexB = i;
		}
	}
	
	// determine connectedness
	if (aggregateYearsRadio.selected) {
		bFollowsA = (indexA + 1) % termSequence.length == indexB;
		aFollowsB = (indexB + 1) % termSequence.length == indexA;
	} else {
		bFollowsA = indexA + 1 == indexB;
		aFollowsB = indexB + 1 == indexA;
	}
	
	// connect nodes appropriately
	if (bFollowsA) {
		response.bFollowsA = true;
		// then b follows a
		if (courseNodeA.data.childNodes[courseNodeB.data.id]) {
			// already added, bump count
			courseNodeA.data.childNodeWeights[courseNodeB.data.id]++;
		} else {
			// add for first time
			courseNodeA.data.childNodes[courseNodeB.data.id] = courseNodeB;
			courseNodeA.data.childNodeWeights[courseNodeB.data.id] = 1;
		}
		if (courseNodeB.data.parentNodes[courseNodeA.data.id]) {
			// already added, bump count
			courseNodeB.data.parentNodeWeights[courseNodeA.data.id]++;
		} else {
			// add for first time
			courseNodeB.data.parentNodes[courseNodeA.data.id] = courseNodeA;
			courseNodeB.data.parentNodeWeights[courseNodeA.data.id] = 1;
		}
	}
	if (aFollowsB) {
		response.aFollowsB = true;
		// then a follows b
		if (courseNodeB.data.childNodes[courseNodeA.data.id]) {
			// already added, bump count
			courseNodeB.data.childNodeWeights[courseNodeA.data.id]++;
		} else {
			// add for first time
			courseNodeB.data.childNodes[courseNodeA.data.id] = courseNodeA;
			courseNodeB.data.childNodeWeights[courseNodeA.data.id] = 1;
		}
		if (courseNodeA.data.parentNodes[courseNodeB.data.id]) {
			// already added, bump count
			courseNodeA.data.parentNodeWeights[courseNodeB.data.id]++;
		} else {
			// add for first time
			courseNodeA.data.parentNodes[courseNodeB.data.id] = courseNodeB;
			courseNodeA.data.parentNodeWeights[courseNodeB.data.id] = 1;
		}
	}
	return response;
}

public function applyFilters():void {
	redrawGraph();
}

public function aggregateYearsRadioHandler(e:Event) :void {
	var aggregated:Boolean = aggregateYearsRadioGroup.selectedValue == aggregateYearsRadio.label;
	resetData(aggregated);
	totalCoursesFilterLabel.text = courseAutoComplete.selectedItems.length + " courses selected";
}

private function handleAutoCompleteChange():void {
	var course:Object = courseAutoComplete.selectedItem;
	totalCoursesFilterLabel.text = courseAutoComplete.selectedItems.length + " courses selected";
}

private function handleItemClick(event:MenuEvent):void {
	switch (String(event.item.@data))
	{
		case "browse":
			courseAutoComplete.showBrowser();
			break;
		case "remove":
			courseAutoComplete.selectedItems.removeAll();
			break;	
		case "loadsuid":
			courseAutoComplete.showSUIDLoader();
			break;
	}
}

public function termsArrContains(termStr:String):Boolean {
	var term:TermListItemValueObject;
	for each (term in termsArr) {
		if (term.label == termStr && term.selected) {
			return true;
		}
	}
	return false;
}

public function coursesFilterContains(courseId:String):Boolean {
	var courseNode:NodeSprite;
	for each (courseNode in courseAutoComplete.selectedItems) {
		if (courseNode.data.id == courseId) {
			return true;
		}
	}
	return false;
}

public function getStudentCourseIntersectionCount(courseNodeA:NodeSprite, courseNodeB:NodeSprite):int {
	var intersectionCount:int = 0;
	var studentA:Student;
	var studentB:Student
	for each (studentA in courseNodeA.data.students) {
		for each (studentB in courseNodeB.data.students) {
			if (studentA.id == studentB.id) {
				intersectionCount++;
			}
		}
	}
	return intersectionCount;
}

public function labelFunction(o:Object):String {
	return o.data.id;
}

public function handleSUIDLoaderChange(event:Event):void
{
	var suidStr:String = event.currentTarget.suidField.text;
	var student:Student = students[suidStr];
	courseAutoComplete.selectedItems.removeAll();
	var termsForStudent:Dictionary = new Dictionary();
	for each (var course:NodeSprite in student.courses) {
		courseAutoComplete.selectedItems.addItem(course);
		termsForStudent[course.data.term] = true;
	}
	for each (var termObj:TermListItemValueObject in termsArr) {
		if (termsForStudent[termObj.label]) {
			termObj.selected = true;
		} else {
			termObj.selected = false;
		}
	}
	//terms.dataProvider.refresh();
	totalCoursesFilterLabel.text = courseAutoComplete.selectedItems.length + " courses selected";
}


public function expandNodeHandler(e:SelectionEvent) :void {
	var selectedNode:NodeSprite = courseNodes[e.item.data.id];
	var parents:Object = { label: "Get parents", data: selectedNode };
	var children:Object = { label: "get children", data: selectedNode };
	// creat the root menu structure
	var menuData:Object = { children: [ parents, children ] };		
	// create menu
	var menu:Menu = Menu.createMenu( parent, menuData, false );
	menu.addEventListener( MenuEvent.ITEM_CLICK, handleMenuClick );
	// show the menu
	menu.show(e.cause.stageX, e.cause.stageY);
}

public function handleMenuClick( menuEvent:MenuEvent ):void {
	if (menuEvent.item.label == "Get parents") {
		expandNode(menuEvent.item.data, true);
	} else {
		expandNode(menuEvent.item.data, false);
	}
}


public function expandNode(selectedNode:NodeSprite, parents:Boolean) :void {
	for each (var otherNode:NodeSprite in courseNodes) {
		if ((setSuccessionRelationships(selectedNode, otherNode).aFollowsB && parents ||
			setSuccessionRelationships(selectedNode, otherNode).bFollowsA && !parents) &&
			!courseAutoComplete.selectedItems.contains(otherNode) &&
			getStudentCourseIntersectionCount(selectedNode, otherNode) > 0) {
			courseAutoComplete.selectedItems.addItem(otherNode);
		}
	}
	totalCoursesFilterLabel.text = courseAutoComplete.selectedItems.length + " courses selected";
	redrawGraph();
}

public function getStudents() :Dictionary {
	return students;
}

public function sliderChangeHandler(event:SliderEvent):void {
	var currentSlider:HSlider = HSlider(event.currentTarget);
	CourseGraphLayout(holder.getChildAt(0)).minEdgeWeightThreshold = currentSlider.values[0];
	CourseGraphLayout(holder.getChildAt(0)).maxEdgeWeightThreshold = currentSlider.values[1];
}

public function layoutTypeRadioHandler(event:Event):void {
	if (forceDirectedRadio.selected) {
		CourseGraphLayout(holder.getChildAt(0)).setForceDirectedLayout();
	} else if (chronologicalRadio.selected) {
		CourseGraphLayout(holder.getChildAt(0)).setChronologicalLayout();
	} else {
		CourseGraphLayout(holder.getChildAt(0)).setForceDirectedLayout(true);	// true to disable forces for a manual layout
	}
}
